<?php

declare(strict_types=1);

namespace sheep\library\install;

use sheep\library\install\traits\Env;
use sheep\library\console\Composer as ComposerProcess;
use sheep\exception\SheepException;
use sheep\facade\Redis;
use think\facade\Config;
use PDOException;
use PDO;
use app\admin\model\auth\Admin;
use yunwuxin\facade\Notification;
use think\facade\Log;

class Install
{

    use Env;

    protected $isCli = false;

    protected $output = null;

    protected $provider = null;

    protected $runtimePath = '';

    public function __construct($output = null)
    {
        $this->output = $output;
        $this->isCli = request()->isCli();
        $this->provider();
        $this->setEnvFilePath();
        $this->runtimePath = root_path('runtime');
    }


    /**
     * 获取要执行的数据填充
     */
    public function getSeeds() {
        // 这里要初始化，默认所有初始化，数据，都放到 InitSheep seed 中
        $seeds = [
            'InitSheep'     // 超级管理组，配置等
        ];

        return $seeds;
    }



    /**
     * 检测系统环境
     * 
     * @return array
     */
    public function checkSystemEnv()
    {
        $checkItem = [];

        $is_ok = true;

        // env 文件是否可以写入
        $env = ['item' => '.env', 'title' => '.env 文件是否可写'];
        if (!$this->checkEnvWritable()) {
            $is_ok = false;
            $env = array_merge($env, ['error' => 1, 'msg' => '不能写入 .env 文件，请检查文件权限后重试']);
        } else {
            $env = array_merge($env, ['error' => 0, 'msg' => '.env 文件可正常写入']);
        }
        $checkItem[] = $env;

        // 检测 runtime 目录是否可写
        $runtime = ['item' => 'runtime', 'title' => 'runtime 目录是否可写'];
        if (!$this->checkRuntimeWritable()) {
            $is_ok = false;
            $runtime = array_merge($runtime, ['error' => 1, 'msg' => 'runtime 目录不能写入，请检查 runtime 目录权限后重试']);
        } else {
            $runtime = array_merge($runtime, ['error' => 0, 'msg' => 'runtime 目录可正常写入']);
        }
        $checkItem[] = $runtime;

        // php 版本是否符合要求
        $php_version = ['item' => 'php_version', 'title' => 'php 版本是否符合要求'];
        if (version_compare(PHP_VERSION, '7.4', '<')) {
            $is_ok = false;
            $php_version = array_merge($php_version, ['error' => 1, 'msg' => '请升级 php 版本为 7.4~8.0']);
        } else {
            $php_version = array_merge($php_version, ['error' => 0, 'msg' => 'php 版本' . PHP_VERSION . '正常[7.4~8.0]', 'version' => PHP_VERSION]);
        }
        $checkItem[] = $php_version;

        // 获取 php 安装的所有扩展
        $extensions = array_map('strtolower', get_loaded_extensions());

        // php 扩展检查

        // fileinfo 扩展
        $fileinfo = ['item' => 'php_fileinfo', 'title' => 'php fileinfo 扩展是否安装'];
        if (!in_array('fileinfo', $extensions)) {
            $is_ok = false;
            $fileinfo = array_merge($fileinfo, ['error' => 1, 'msg' => '请安装 php fileinfo 扩展']);
        } else {
            $fileinfo = array_merge($fileinfo, ['error' => 0, 'msg' => 'php fileinfo 扩展']);
        }
        $checkItem[] = $fileinfo;

        // redis 扩展
        $redis = ['item' => 'php_redis', 'title' => 'php redis 扩展是否安装'];
        if (!in_array('redis', $extensions)) {
            $is_ok = false;
            $redis = array_merge($redis, ['error' => 1, 'msg' => '请安装 php redis 扩展']);
        } else {
            $redis = array_merge($redis, ['error' => 0, 'msg' => 'php redis 扩展']);
        }
        $checkItem[] = $redis;

        // zip 扩展
        // $zip = ['item' => 'php_zip', 'title' => 'php zip 扩展是否安装'];
        // if (!in_array('zip', $extensions)) {
        //     $is_ok = false;
        //     $zip = array_merge($zip, ['error' => 1, 'msg' => '请开启 php zip 扩展']);
        // } else {
        //     $zip = array_merge($zip, ['error' => 0, 'msg' => 'php zip 扩展']);
        // }
        // $checkItem[] = $zip;

        // pdo_mysql
        $pdo_mysql = ['item' => 'php_pdo_mysql', 'title' => 'php pdo_mysql 扩展是否安装'];
        if (!in_array('pdo_mysql', $extensions)) {
            $is_ok = false;
            $pdo_mysql = array_merge($pdo_mysql, ['error' => 1, 'msg' => '请开启 php pdo_mysql 扩展']);
        } else {
            $pdo_mysql = array_merge($pdo_mysql, ['error' => 0, 'msg' => 'php pdo 和 pdo_mysql 扩展']);
        }
        $checkItem[] = $pdo_mysql;

        // openssl
        $openssl = ['item' => 'php_openssl', 'title' => 'php openssl 扩展是否安装'];
        if (!in_array('openssl', $extensions)) {
            $is_ok = false;
            $openssl = array_merge($openssl, ['error' => 1, 'msg' => '请开启 php openssl 扩展']);
        } else {
            $openssl = array_merge($openssl, ['error' => 0, 'msg' => 'php openssl 扩展']);
        }
        $checkItem[] = $openssl;

        // 禁用函数
        $disable_functions = ['item' => 'disable_functions', 'title' => '特殊函数是否正常'];
        if ($needOpenFunctions = $this->checkDisabledFunction()) {
            $is_ok = false;
            $disable_functions = array_merge($disable_functions, ['error' => 1, 'msg' => '请删除被禁用的函数 ' . join(',', $needOpenFunctions)]);
        } else {
            $disable_functions = array_merge($disable_functions, ['error' => 0, 'msg' => '特殊函数可正常使用']);
        }
        $checkItem[] = $disable_functions;

        // 如果 exec 被禁用了，先不检测 composer
        if (!in_array('exec', $needOpenFunctions)) {
            // 检测是否安装了 composer
            $composer = ['item' => 'composer', 'title' => 'composer 是否安装'];
            $composerProcess = (new ComposerProcess);
            if ($composerProcess->isInstalled()) {
                $version = $composerProcess->getVersion($composerProcess->getOutput());
                // 使用阿里云镜像
                $composerProcess->addRepository('packagist', 'composer', 'https://mirrors.aliyun.com/composer/');
                $composer = array_merge($composer, ['error' => 0, 'msg' => 'composer 已安装', 'version' => $version]);
            } else {
                $is_ok = false;
                $composer = array_merge($composer, ['error' => 1, 'msg' => '请先安装 composer']);
            }
            $checkItem[] = $composer;
        }

        // ssl
        $https = ['item' => 'https', 'title' => '是否配置 https 证书', 'msg' => 'https 配置成功'];
        if (!$this->isCli) {
            $is_ssl = request()->isSsl();
            $https['error'] = $is_ssl ? 0 : 2;
            $https['msg'] = $is_ssl ? $https['msg'] : '请配置 https 证书';
        } else {
            $https['error'] = 2;
            $https['msg'] = '命令行跳过检测 https';
        }
        $checkItem[] = $https;

        $info = $this->provider->envOutput($checkItem);
        $result = [
            'error' => $is_ok ? 0 : 1,
            'output' => $info
        ];

        return $result;
    }


    /**
     * 界面安装数据库 redis配置
     *
     * @return void
     */
    public function viewConfig() {
        try {
            // 设置传来的数据库账号密码
            $this->provider->setDatabaseConfig();
            // 检测数据库
            $this->checkDatabase();
        } catch (\Exception $e) {
            throw new SheepException("数据库配置错误：" . $e->getMessage());
        }

        // 检测redis
        $check_redis = request()->param('check_redis', 0);
        if ($check_redis) {
            try {
                // 设置传来的redis 参数
                $this->provider->setRedisConfig();
                // 检测redis
                $this->checkRedis();
            } catch (\Exception $e) {
                throw new SheepException("redis 配置错误：" . $e->getMessage());
            }
        }
    }




    /**
     * 更新扩展包
     */
    public function updatePackages()
    {
        $composer = new ComposerProcess;
        if ($this->isCli) {
            $composer->setCallback(function ($message) {
                echo $message;
            });
        }
        $result = $composer->update();
        return $result;
    }


    /**
     * 检测数据库账号密码是否正确
     */
    public function checkDatabase()
    {
        $hostname = Config::get("database.connections.mysql.hostname");
        $hostport = Config::get("database.connections.mysql.hostport");
        $database = Config::get("database.connections.mysql.database");
        $username = Config::get("database.connections.mysql.username");
        $password = Config::get("database.connections.mysql.password");

        if (!strlen($hostname) || !strlen($database)) {
            throw new SheepException('请完善 mysql 信息');
        }

        // 检查连接
        $dsn = 'mysql:host=' . $hostname;
        if ($hostport) $dsn .= ";port=" . $hostport;

        try {
            $pdo = new PDO($dsn, $username, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

            $mysql_version = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
            if (version_compare($mysql_version, '5.7.0', '<')) {
                throw new SheepException('mysql 最小版本为 5.7.0，当前版本为 ' . $mysql_version);
            }

            // 判断数据库是否存在，如果不存在，创建
            $databases = $pdo->query("show databases");
            $database_exists = false;
            foreach($databases as $db) {
                if ($db['Database'] == $database) {
                    $database_exists = true;
                    break;
                }
            }

            if (!$database_exists) {
                // 创建数据库
                $result = $pdo->exec("CREATE DATABASE IF NOT EXISTS `{$database}` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;");
                if (!$result) {
                    throw new SheepException('数据库创建失败');
                }
            }
        } catch (\Exception $ex) {
            $message = $ex->getMessage();
            if ($ex instanceof PDOException) {
                $message = 'Connection failed: ' . $ex->getMessage();
            }
            // $message = 'mysql 链接失败';
            throw new SheepException($message);
        }
    }


    /**
     * 检测 redis 配置是否正确
     */
    public function checkRedis()
    {
        $hostname = Config::get("queue.connections.redis.host");
        $hostport = Config::get("queue.connections.redis.port");
        $password = Config::get("queue.connections.redis.password");
        if (!strlen($hostname)) {
            throw new SheepException('未填写 redis 连接地址');
        }

        try {
            // 检测是否配置成功
            $result = Redis::ping();
        } catch (\Exception $ex) {
            throw new SheepException('Connection failed: ' . $ex->getMessage());
        }
    }



    /**
     * 执行数据迁移
     */
    public function databaseMigrate()
    {
        // 执行数据迁移，并将所有输出导入到 output 中
        exec(get_php() . ' ' . root_path() . 'think migrate:run 2>&1', $output);

        // 过滤空值，并输出结果
        $output = array_values(array_filter($output));

        if ($this->isCli) {
            foreach ($output as $o) {
                $this->output->writeln($o);
            }
        }

        $result = [
            'error' => strpos(end($output), 'All Done') !== false ? 0 : 1,
            'output' => $output
        ];

        return $result;
    }



    /**
     * 初始数据填充,非测试数据
     */
    public function databaseSeed()
    {
        // 获取数据填充类
        $seeds = $this->getSeeds();

        // 是否全部执行成功
        $is_ok = true;
        // 所有填充执行的状态
        $statusItems = [];
        foreach ($seeds as $seed) {
            $statusItem = [];
            // 执行数据迁移，并将所有输出导入到 output 中
            $output = [];       // 初始化 output
            exec(get_php() . ' ' . root_path() . 'think seed:run --seed=' . $seed . ' 2>&1', $output);

            // 过滤空值，并输出结果
            $output = array_values(array_filter($output));

            $statusItem['item'] = $seed;
            $statusItem['output'] = $output;

            // 如果是命令行，直接输出结果
            if ($this->isCli) {
                foreach ($output as $o) {
                    $this->output->writeln($o);
                }
            }

            $statusItem['error'] = 0;
            if (strpos(end($output), 'All Done') === false) {
                $is_ok = false;
                $statusItem['error'] = 1;

                $this->isCli && $this->output->error('[no] ' . $seed . ' 执行失败');
            }

            $statusItems[] = $statusItem;
        }

        $result = [
            'error' => $is_ok ? 0 : 1,
            'statusItem' => $statusItems
        ];

        return $result;
    }


    /**
     * 初始数据填充,非测试数据
     */
    public function databaseTestSeed()
    {
        // 执行数据迁移，并将所有输出导入到 output 中
        $output = [];       // 初始化 output
        exec(get_php() . ' ' . root_path() . 'think seed:run --seed=TestData 2>&1', $output);

        // 过滤空值，并输出结果
        $output = array_values(array_filter($output));

        // 如果是命令行，直接输出结果
        if ($this->isCli) {
            foreach ($output as $o) {
                $this->output->writeln($o);
            }
        }

        $error = 0;
        if (strpos(end($output), 'All Done') === false) {
            $error = 1;

            $this->isCli && $this->output->error('[no] 测试数据安装失败');
        }

        return compact('error');
    }



    public function createAdmin($account, $password) 
    {
        $admin = Admin::where('account', $account)->find();
        if (!$admin) {
            $salt = mt_rand(1000, 9999);
            
            $admin = new Admin();
            $admin->account = $account;
            $admin->nickname = $account;
            $admin->avatar = '/static/img/admin/avatar.png';
            $admin->salt = $salt;
            $admin->password = $admin->encryptPassword($password, $salt);
            $admin->role_id = 1;
            $admin->save();

            return true;
        }

        return false;
        // 将管理员加入超级组
        // Enforcer::addRoleForUser('admin:' . $admin->id, 'super');
    }


    /**
     * 安装成功方法
     *
     * @return void
     */
    public function success() 
    {
        $sign = \sheep\model\Config::getConfigField('basic.site.sign');

        if (!$sign) {
            \sheep\model\Config::setConfigs('basic.site', ['sign' => mt_rand(1000, 9999)]);
        }

        if ($this->isCli) {
            // 修改项目目录权限
            $this->permission();
        }

        // 发送一条管理员通知
        $admins = Admin::select();
        if (!$admins->isEmpty()) {
            Notification::send(
                $admins,
                new \sheep\notification\CustomeNotice([
                    'receiver_type' => 'admin',
                    'notification_type' => 'system'
                ], [
                    'message_title' => '安装完成',
                    'message_text' => '恭喜您,系统已安装完成。'
                ])
            );
        }
        return true;
    }


    /**
     * 修改项目目录权限
     *
     * @return void
     */
    public function permission()
    {
        if (strpos(strtolower(PHP_OS), 'linux') !== false) {
            // 修改项目目录权限
            exec('chown -R www:www ' . root_path() . ' 2>&1', $output);

            $output = array_values(array_filter($output));
            foreach ($output as $o) {
                $this->output->warning($o);
            }
        }
    }


    /**
     * 写入安装锁定文件
     */
    public function writeInstallLock()
    {
        $path = root_path() . 'sheep' . DIRECTORY_SEPARATOR . 'library' . DIRECTORY_SEPARATOR . 'install' . DIRECTORY_SEPARATOR . 'install.lock';
    
        file_put_contents($path, date('Y-m-d H:i:s'));

        return true;
    }


    /**
     * 检测 env 文件是否可写
     */
    public function checkEnvWritable()
    {
        // 复制 env 文件，并加载配置
        if (!file_exists($this->envFilePath)) {
            copy(root_path() . '.env.example', $this->envFilePath);
            $this->refreshEnvVars();
        }

        return is_writable($this->envFilePath);
    }


    /**
     * 检测 runtime 是否可写
     */
    protected function checkRuntimeWritable()
    {
        return is_writable($this->runtimePath);
    }


    protected function checkDisabledFunction() 
    {
        // 被禁用的函数
        $ini_get = ini_get('disable_functions');
        $disableFunctions = array_values(array_filter(explode(',', $ini_get)));
        // 需要的函数
        $needFunctions = [
            'exec',
            'putenv',
            'shell_exec',
            'proc_open',
            'popen',
            'pcntl_alarm',
            'pcntl_fork',
            'pcntl_wait',
            'pcntl_signal',
            'pcntl_signal_dispatch',
        ];

        // 返回被禁用的必须要开放的函数数组
        return array_values(array_intersect($disableFunctions, $needFunctions));
    }


    protected function provider()
    {
        $provider = $this->isCli ? 'console' : 'view';

        $class = "\\sheep\\library\\install\\provider\\" . ucfirst($provider);

        $this->provider = new $class($this, $this->output);

        return $this->provider;
    }



    public function __call($funcname, $arguments)
    {
        return $this->provider->{$funcname}(...$arguments);
    }
}
